注意:所有文章除特别说明外,转载请注明出处.
[TOC]
第二章 Java并发机制的底层实现原理
2.1 volatile的应用
在多线程并发编程中 synchronize 和 volatile 扮演重要角色。 volatile 是轻量级的 synchronize ,在多处理器中保证共享变量的可见性。
如果使用 volatile 变量修饰符使用恰当,比 synchronize 的使用和执行成本更低。因为它不会引起线程上下文的切换和调度。
2.2.1 volatile定义与实现原理
定义:Java编程语言允许线程访问共享变量,为了确保共享变量能够被准确和一致地更新,线程应该确保通过排它锁单独获取这个变量。
CPU操作术语:
1.内存屏障:是一组处理器指令,用于实现对内存操作的顺序限制。
...
2.2 synchronize的实现原理和应用
synchronize实现同步的基础:Java中每一个对象都可以作为锁。
1.对于普通同步方法,锁是当前实例对象。
2.对于静态同步方法,锁是当前静态类的Class对象。
3.对于同步方法块,锁是synchronize括号里配置的对象。
提示:在JVM中,代码块同步是使用monitorenter和monitorexit指令实现的。
2.2.1 Java对象头
synchronize 用的锁是存在Java对象头里。如果对象是数组类型,则虚拟机用3个字宽存储对象头,如果是非数组类型,则用2字宽存储对象头。
2.2.2 锁的升级和对比
Java 1.6中,锁有四种状态,级别从低到高是:无锁状态、偏向锁状态、轻量级锁状态、重量级锁状态。
注意:锁可以升级,不可以降级。如:偏向级锁升级成轻量级锁之后不能降为偏向锁。
1.偏向锁
当一个线程访问同步块并获取锁时,会在对象头和栈帧中的锁记录里存储锁偏向的线程ID,以后该线程在进入和退出同步块时不需要进行CAS操作来加锁和解锁,只需要简单测试下对象头的mark word里是否存有指向当前线程的偏向锁。
2.轻量级锁
线程在执行同步块之前,JVM会先在当前线程的栈帧中创建用于存储锁记录的空间,并将对象头中的 Mark Word 复制到锁记录中。然后尝试使用CAS将对象头中的 Mark Word 替换为指向锁记录的指针。如果成功,当前线程获得锁,如果失败,表示其它现车个竞争锁。
3.锁之间的对比
锁 | 优点 | 缺点 | 适用场景 |
---|---|---|---|
偏向锁 | 加锁和解锁不需要额外的开销,和执行非同步方法相比仅存在纳秒级差距 | 如果线程之间存在锁竞争,会带来额外的锁撤销的消耗 | 适用于一个线程访问同步块场景 |
轻量级锁 | 竞争的线程不会阻塞,提高了程序的响应速度 | 如果始终得不到锁竞争的线程,适用自旋会消耗CPU | 追求响应时间同步块执行速度非常快 |
重量级锁 | 线程竞争不使用自旋,不会消耗CPU | 线程阻塞,响应时间缓慢 | 追求吞吐量,同步块执行速度较长 |
2.3 原子操作实现原理
2.3.3 Java实现原子操作
在Java中通过锁和循环CAS的方式实现原子操作。
1.使用CAS实现原子操作
JVM中的CAS操作正是利用了处理器提供 CMPXCHG 指令实现的,自旋CAS实现的基本思路就是循环进行CAS操作直到成功为止。
提示:在Java 1.5以后,JDK的并发包提供一些类支持原子操作,如:AtomicBoolean(用原子方式更新的boolean值)、AtomicInteger(用原子方式更新int值)、AtomicInteger(用原子方式更新Long值)。
2.CAS实现原子操作的三大问题
1.ABA问题:因为CAS需要操作值时,检查值有没有发生变化,如果没有则更新,但如果值原来是A,后变成B,后又变成A,那么使用CAS进行检查时发现它的值没有发生变化,实际上变化了。
解决:ABA问题使用版本号解决,每次更新的时候将版本号加1。在Java 1.5之后,JDK的Atomic包提供一个类 AtomicStampedReferance 解决ABA问题。
2.循环时间长开销大:自旋CAS如果长时间不成功,会给CPU带来非常大的执行开销。如果JVM提供pause指令,那么效率会提升。
3.只能保证一个共享变量的原子操作:当对一个共享变量执行操作时,我们可以使用循环CAS来保证原子操作,但是对于多个共享变量时,这时候CAS无法保证操作的原子性,这时候可以用锁。或者将多个共享变量合并成一个共享变量来操作。
3.使用锁机制来实现原子操作
锁机制保证了只有获得锁的线程才能操作锁定的内存区域。